Définition : Une closure - ou fermetures - est une portion autonome de code qui peut être passée en tant que valeur à une fonction ou stockée dans une constante ou une variable. Les closures peuvent capturer et stocker les valeurs de leur environnement, ce qui les rend plus flexibles que les simples fonctions.

La syntaxe de base d'un closure en Swift ressemble à ceci :


{ (paramètres) -> type de retour in
    instructions
}

Les closures prennent l'une des trois formes suivantes :

  • Les fonctions globales sont des closures qui ont un nom et ne capturent aucune valeur.
  • Les fonctions imbriquées sont des closures qui ont un nom et peuvent capturer des valeurs à partir de leur fonction englobante.
  • Les expressions de Closure sont des closures sans nom écrites dans une syntaxe légère qui peuvent capturer des valeurs de leur contexte environnant.

La bibliothèque standard de Swift fournit une méthode appelée sorted(by:), qui trie un tableau de valeurs d'un type connu, en fonction de la sortie d'une closure de tri que vous fournissez. Une fois le processus de tri terminé, la méthode sorted(by:) renvoie un nouveau tableau du même type et de la même taille que l'ancien, avec ses éléments dans le bon ordre de tri. Le tableau d'origine n'est pas modifié par la méthode sorted(by:).

Les exemples d'expression de closure ci-dessous utilisent la méthode sorted(by:) pour trier un tableau de valeurs String dans l'ordre alphabétique inverse. Voici le tableau initial à trier :


let names = ["Christian", "Alexandra", "Ernest", "Bernard", "Daniel"]

La méthode sorted(by:) accepte une closure qui prend deux arguments du même type que le contenu du tableau et renvoie une valeur Bool pour indiquer si la première valeur doit apparaître avant ou après la deuxième valeur une fois que les valeurs sont triées.
La closure de tri devra renvoyée true si la première valeur doit apparaître avant la deuxième valeur, et false sinon.

Cet exemple trie un tableau de valeurs String, et donc la closure de tri doit être une fonction de type (String, String) -> Bool.

Si la première chaîne s1 est supérieure à la deuxième chaîne s2, la fonction backward(_:_:) renverra true, indiquant que s1 doit apparaître avant s2 dans le tableau trié. Pour les caractères dans les chaînes, "supérieur à" signifie "apparaît plus tard dans l'alphabet que". Cela signifie que la lettre "B" est "plus grande que" la lettre "A"et que la chaîne "Tom"est supérieure à la chaîne "Tim". Cela donne un tri alphabétique inversé, en plaçant "Bernard" avant "Alexandra", et ainsi de suite.

Cependant, il s'agit d'une manière assez longue d'écrire ce qui est essentiellement une fonction à expression unique ( ). Dans cet exemple, il serait préférable d'écrire la closure de tri en ligne, en utilisant la syntaxe d'expression de fermeture a > b. Voici comment faire.


func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// ["Ernest", "Daniel", "Christian", "Bernard", "Alexandra"]

Dans cet exemple, il serait préférable d'écrire la closure de tri en ligne, en utilisant la syntaxe d'expression de closure : a > b. Voici comment faire.

Syntaxe d'une Closure


{ (parameters) -> return type in
    statements
}

Les paramètres de la syntaxe d'expression de closure peuvent être des paramètres d'entrée-sortie (inout), mais ils ne peuvent pas avoir de valeur par défaut. Les paramètres variadiques peuvent être utilisés si vous nommez le paramètre variadique. Les tuples peuvent également être utilisés comme types de paramètres et types de retour.


reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

La déclaration des paramètres et du type de retour pour cette closure en ligne est identique à la déclaration de la fonction backward(_:_:)

Le début du corps de la closure est introduit par le mot-clé in. Ce mot clé indique que la définition des paramètres de la closure et du type de retour est terminée et que le corps de la closure est sur le point de commencer.

Parce que le corps de la closure est si court, il peut même être écrit sur une seule ligne :


reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

Cela montre que l'appel global à la méthode sorted(by:) est resté le même. Une paire de parenthèses encapsule toujours l'argument entier de la méthode. Cependant, cet argument est maintenant une closure en ligne.

Déduire le type d'une closure à partir du contexte

Étant donné que la closure de tri est passée en tant qu'argument à une méthode, Swift peut déduire les types de ses paramètres et le type de la valeur qu'elle renvoie. Étant donné que tous les types peuvent être déduits, la flèche de retour (->) et les parenthèses autour des noms des paramètres peuvent également être omises :


reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
Il est toujours possible de déduire les types de paramètres et le type de retour lors du passage d'une closure à une fonction ou à une méthode en tant qu'expression de closure en ligne. Par conséquent, vous n'avez jamais besoin d'écrire une closure en ligne dans sa forme la plus complète lorsque la closure est utilisée comme argument de fonction ou de méthode.

Néanmoins, vous pouvez toujours rendre les types explicites si vous le souhaitez, et cela est encouragé si cela évite toute ambiguïté pour les lecteurs de votre code.

Retour implicite de closure à expression unique

Les closures d'expression unique peuvent renvoyer implicitement le résultat de leur expression unique en omettant le mot-clé return dans leur déclaration :


reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

Étant donné que le corps de la closure ne contient qu'une seule expression s1 > s2 qui renvoie une valeur booléenne, il n'y a pas d'ambiguïté et le mot-clé return peut être omis.

Noms d'arguments abrégés

Swift fournit automatiquement des noms d'arguments raccourcis à des closures alignées, qui peuvent être utilisés pour faire référence aux valeurs des arguments de la closure par les noms $0, $1, $2...

Si vous utilisez ces noms d'arguments abrégés dans votre expression de fermeture, vous pouvez omettre la liste d'arguments de la closure de sa définition. Le type des noms d'arguments abrégés est déduit du type de fonction attendu, et l'argument abrégé le plus élevé que vous utilisez détermine le nombre d'arguments pris par la fermeture. Le mot-clé in peut également être omis, car l'expression de fermeture est entièrement constituée de son corps :


reversedNames = names.sorted(by: { $0 > $1 } )

Méthode de l'opérateur

Il existe un moyen encore plus court d'écrire l'expression de closure ci-dessus. Le type String de Swift définit son implémentation spécifique à la chaîne de l'opérateur supérieur à (>) comme une méthode qui a deux paramètres de type String, et renvoie une valeur de type Bool.

Cela correspond exactement au type de méthode requis par la méthode sorted(by:). Par conséquent, vous pouvez simplement passer l'opérateur supérieur (>), et Swift en déduira que vous souhaitez utiliser son implémentation spécifique à la chaîne :


reversedNames = names.sorted(by: >)

Trailing closure

Si vous devez passer une expression de closure à une fonction comme argument final de la fonction et que l'expression de closure est longue, il peut être utile de l'écrire comme une Trailing closure à la place.


func someFunctionThatTakesAClosure(closure: () -> Void) {
    // Corps de la fonction
}

// Appel de la fonction SANS closure de fin
someFunctionThatTakesAClosure(closure: {
    // Corps de la closure
})

// Appel de la fonction AVEC une closure de fin
someFunctionThatTakesAClosure() {
    // Corps de la Trailing closure
}

La closure de tri de la chaîne précédente peut être écrite en dehors des parenthèses de la méthode sorted(by:) comme une closure de fin ou trailing closure:


reversedNames = names.sorted() { $0 > $1 }

Si une expression de closure est fournie comme seul argument de la fonction ou de la méthode et que vous fournissez cette expression comme closure de fin, vous n'avez pas besoin d'écrire une paire de parenthèses après le nom de la fonction ou de la méthode lorsque vous appelez la fonction :


reversedNames = names.sorted { $0 > $1 }

Les closures de fin plus sont utiles lorsque la closure est suffisamment longue pour qu'il ne soit pas possible de l'écrire sur une seule ligne.

Par exemple, le type Array de Swift a une méthode map(_:), qui prend une expression de closure comme argument unique. La closure est appelée une fois pour chaque élément du tableau et renvoie une valeur mappée alternative (éventuellement d'un autre type) pour cet élément.

Voici comment vous pouvez utiliser la méthode map(_:) avec une closure finale pour convertir un tableau de valeurs Int en un tableau de valeurs String. Le tableau [16, 58, 510] est utilisé pour créer le nouveau tableau ["UnSix", "CinqHuit", "CinqUnZero"] :


let digitNames = [
    0: "Zero", 1: "Un", 2: "Deux",   3: "Trois", 4: "Quatre",
    5: "Cinq", 6: "Six", 7: "Sept", 8: "Huit", 9: "Neuf"
]
let numbers = [16, 58, 510]

Vous pouvez maintenant utiliser le tableau numbers pour créer un tableau de valeurs String, en passant une expression de closure à la méthode map(_:) du tableau comme closure de fin :


let strings = numbers.map { (number) -> String in
    var number = number
    var output = ""
    repeat {
        output = digitNames[number % 10]! + output
        number /= 10
    } while number > 0
    return output
}
print(strings)
// les chaînes sont supposées être de type [String]
// ["UnSix", "CinqHuit", "CinqUnZero"]

La méthode map(_:) appelle l'expression de closure une fois pour chaque élément du tableau. Vous n'avez pas besoin de spécifier le type du paramètre d'entrée de la closure number, car le type peut être déduit des valeurs du tableau à mapper.

La variable number est initialisée avec la valeur du paramètre number de la closure, de sorte que la valeur puisse être modifiée dans le corps de la closure. (Les paramètres des fonctions et des closures sont toujours des constantes.)

L'expression de closure spécifie également un type de retour String, pour indiquer le type qui sera stocké dans le tableau de sortie mappé.

L'expression de closure crée une chaîne appelée output à chaque fois qu'elle est appelée. Il calcule le dernier chiffre de number à l'aide de l'opérateur reste et utilise ce chiffre pour rechercher une chaîne appropriée dans le dictionnaire.

L'appel à l'indice digitNames du dictionnaire est suivi d'un point d'exclamation (!), car les indices du dictionnaire renvoient une valeur facultative pour indiquer que la recherche dans le dictionnaire peut échouer si la clé n'existe pas. Dans l'exemple ci-dessus, il est garanti que ce sera toujours une clé d'indice valide pour le dictionnaire, et donc un point d'exclamation est utilisé pour forcer le déballage de la valeur stockée.

La chaîne extraite du dictionnaire digitNames est ajoutée au début de output, créant effectivement une version chaîne du nombre à l'envers. (L'expression number % 10 donne une valeur de 6 pour 16, 8 pour 58 et 0 pour 510.)

La variable number est ensuite divisée par 10. Parce que c'est un entier, il est arrondi pendant la division, donc 16 devient 1, 58 devient 5 et 510 devient 51.

Capturer des valeurs

Une closure peut capturer des constantes et des variables du contexte environnant dans lequel elle est définie. La closure peut alors faire référence et modifier les valeurs de ces constantes et variables à partir de son corps, même si la portée d'origine qui définissait les constantes et les variables n'existent plus.

Dans Swift, la forme la plus simple d'une closure qui peut capturer des valeurs est une fonction imbriquée, écrite dans le corps d'une autre fonction. Une fonction imbriquée peut capturer tous les arguments de sa fonction externe et peut également capturer toutes les constantes et variables définies dans la fonction externe.

Voici l'exemple d'une fonction appelée makeIncrementer, qui contient une fonction imbriquée appelée incrementer. La fonction imbriquée incrementer() capture deux valeurs, runningTotal et amount, à partir de son contexte environnant. Après avoir capturé ces valeurs, incrementer est renvoyé par makeIncrementer sous la forme d'une fermeture qui incrémente runningTotal par amount chaque fois qu'il est appelé.


func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

Le type de retour de makeIncrementer est () -> Int. Cela signifie qu'il renvoie une fonction plutôt qu'une simple valeur. La fonction qu'il renvoie n'a aucun paramètre et renvoie une valeur Int à chaque fois qu'elle est appelée.

La fonction makeIncrementer(forIncrement:) définit une variable appelée runningTotal, pour stocker le total cumulé actuel de l'incrémenteur qui sera renvoyé. Cette variable est initialisée avec une valeur de 0.

La fonction makeIncrementer(forIncrement:) a un paramètre Int unique avec une étiquette d'argument forIncrement et un nom de paramètre amount. La valeur d'argument passée à ce paramètre définie de combien runningTotal doit être incrémenté à chaque fois que la fonction est appelée. Cette fonction makeIncrementer définit une fonction imbriquée appelée incrementer, qui effectue l'incrémentation à proprement dite. Cette fonction ajoute simplement amount à runningTotal et renvoie le résultat.

Si on isole la fonction imbriquée incrementer(), elle semble inhabituelle :


func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}

La fonction incrementer() n'a pas de paramètres, et pourtant elle fait référence à runningTotal et amount depuis son corps de fonction. En fait, elle capture une référence de runningTotal et amount depuis la fonction environnante et les utilise dans son propre corps de fonction. La capture par référence garantit que runningTotal et amount ne disparaissent pas lorsque l'appel à makeIncrementer se termine, et garantit également que runningTotal sera disponible la prochaine fois que la fonction incrementer sera appelée.


let incrementByTen = makeIncrementer(forIncrement: 10)

incrementByTen() 
// Retourne une valeur de 10

incrementByTen() 
// Retourne une valeur de 20

Si vous créez un deuxième incrémenteur, il aura sa propre référence stockée à une nouvelle variable runningTotal distincte :


let incrementBySeven = makeIncrementer(forIncrement: 7)

incrementBySeven() 
// Retourne une valeur de 7

L'appel de l'incrémenteur d'origine incrementByTen à nouveau continue d'incrémenter sa propre variable runningTotal et n'affecte pas la variable capturée par incrementBySeven :


incrementByTen()  
// Retourne une valeur de 30

Type de référence

Dans l'exemple ci-dessus, incrementBySeven et incrementByTen sont des constantes, mais les closures auxquelles ces constantes se réfèrent sont toujours capables d'incrémenter les variables runningTotal qu'elles ont capturées. En effet, les fonctions et les closures sont des types de références.

Cela signifie également que si vous affectez une closure à deux constantes ou variables différentes, ces deux constantes ou variables font référence à la même closure.

let alsoIncrementByTen = incrementByTen

alsoIncrementByTen() 
// Retourne une valeur de 50

incrementByTen() 
// Retourne une valeur de 60

L'exemple ci-dessus montre que l'appel alsoIncrementByTen est identique à l'appel incrementByTen. Étant donné que les deux font référence à la même closure, ils incrémentent et renvoient le même total cumulé.

Échappement de closure

On dit qu'une closure échappe à une fonction lorsque la closure est passée comme argument à la fonction, mais est appelée après le retour de la fonction.

Lorsque vous déclarez une fonction qui prend une closure comme l'un de ses paramètres, vous pouvez écrire @escaping avant le type du paramètre pour indiquer que la closure est autorisée à s'échapper.

Une façon dont une closure peut s'échapper est d'être stockée dans une variable définie en dehors de la fonction.

Par exemple, de nombreuses fonctions qui démarrent une opération asynchrone prennent un argument de closure comme gestionnaire d'achèvement. La fonction retourne après avoir démarré l'opération, mais la closure n'est pas appelée tant que l'opération n'est pas terminée - la closure doit s'échapper, pour être appelée plus tard. Par exemple:


var completionHandlers = [() -> Void]()
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

La fonction someFunctionWithEscapingClosure(_:) prend une closure comme argument et l'ajoute à un tableau déclaré en dehors de la fonction. Si vous ne marquez pas le paramètre de cette fonction avec @escaping, vous obtiendrez une erreur de compilation.

Autoclosure

Un autoclosure est une closure qui est créée automatiquement pour envelopper une expression qui a été transmise comme un argument d'une fonction. Il ne prend aucun argument, et quand il est appelé, il renvoie la valeur de l'expression qui y est enveloppée.

Une autoclosure vous permet de retarder l'évaluation, car le code à l'intérieur n'est pas exécuté tant que vous n'appelez pas la closure. Retarder l'évaluation est utile pour le code qui a des effets secondaires ou qui est coûteux en calcul, car il vous permet de contrôler le moment où ce code est évalué :


var customersInLine = ["Christian", "Alexandra", "Eva", "Bernard", "Daniel"]
print(customersInLine.count)  
// Affiche "5"

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)  
// Affiche "5"

print("Maintenant, au tour de \(customerProvider())!")
// Affiche "Maintenant, au tour de Christian !"

print(customersInLine.count)  
// Affiche "4"

Même si le premier élément du tableau customersInLine est supprimé par le code à l'intérieur de la closure (ou fermeture), l'élément du tableau n'est pas supprimé tant que la closure n'est pas réellement appelée. Si la fermeture n'est jamais appelée, l'expression à l'intérieur de la fermeture n'est jamais évaluée, ce qui signifie que l'élément de tableau n'est jamais supprimé. Notez que le type de customerProvider n'est pas String mais () -> String — une fonction sans paramètres qui renvoie une chaîne.

Vous obtenez le même comportement d'évaluation différée lorsque vous passez une closure comme argument à une fonction.


// customersInLine = ["Alexandra", "Eva", "Bernard", "Daniel"]
func serve(customer customerProvider: () -> String) {
    print("Maintenant, au tour de  \(customerProvider())!")
}

serve(customer: { customersInLine.remove(at: 0) } )
// "Maintenant, au tour de Alexandra!"

La fonction serve(customer:) dans l'exemple ci-dessus prend une closure explicite qui renvoie le nom d'un client. Dans la version ci-dessous serve(customer:) effectue la même opération mais, au lieu de prendre une fermeture explicite, elle prend une fermeture automatique en marquant le type de son paramètre avec l'attribut @autoclosure. Vous pouvez maintenant appeler la fonction comme si elle prenait un argument String au lieu d'une closure. L'argument est automatiquement converti en closure, car le type du paramètre customerProvider est marqué avec l'attribut @autoclosure.


// customersInLine = [ "Eva", "Bernard", "Daniel"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Maintenant, au tour de \(customerProvider())!")
}

serve(customer: customersInLine.remove(at: 0))
// "Maintenant, au tour de Eva!"
Une utilisation excessive des fermetures automatiques peut rendre votre code difficile à comprendre. Le contexte et le nom de la fonction doivent indiquer clairement que l'évaluation est différée.

Si vous souhaitez qu'une closure automatique soit autorisée à s'échapper, utilisez à la fois les attributs @autoclosure et @escaping.


// customersInLine = ["Bernard", "Daniel"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
}

collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))

print("\(customerProviders.count) closures collectées.")
// "2 closures collectées."

for customerProvider in customerProviders {
    print("Maintenant, au tour de \(customerProvider())!")
}
// "Maintenant, au tour de Bernard!"
// "Maintenant, au tour de Daniel!"

Dans le code ci-dessus, au lieu d'appeler la fermeture qui lui est passée comme argument customerProvider, la fonction collectCustomerProviders(_:) ajoute la fermeture au tableau customerProviders. Le tableau est déclaré en dehors de la portée de la fonction, ce qui signifie que les fermetures du tableau peuvent être exécutées après le retour de la fonction. Par conséquent, la valeur de l'argument customerProvider doit être autorisée à s'échapper de la portée de la fonction.